React 的 Reconciliation 演算法能將樹比對的複雜度降至 O(n),全靠以下三大策略:
假設我們有一個列表:['A', 'B'],現在想在最前面插入 'X',變成 ['X', 'A', 'B']。
無 key 情況下的比對:若沒有 key,React 只能依序按索引 (Index) 進行比對:
'A',現在變成 'X'-> 修改內容,將 A 改為 X。'B',現在變成 'A' -> 修改內容,將 B 改為 A。'B' -> 新增 DOM 節點 'B'。原本只是「移動」兩個節點並「新增」一個節點,React 卻執行了 2 次 DOM 修改 + 1 次 DOM 新增。如果列表節點帶有 Input 輸入框或內部 State,這些狀態將全數錯位!
當我們為每個項目提供唯一的 key 屬性時:
// 舊:[{ key: 'a' }, { key: 'b' }]
// 新:[{ key: 'x' }, { key: 'a' }, { key: 'b' }]
有唯一 key 的比對:
React 透過 Map 尋找對應的 key:
發現 key 'x' 是全新的 ->新建 DOM 節點 'X' 並插入最前面。
發現 key 'a' 與 'b' 已經存在-> 完全保留 DOM 節點與內部 State,僅做位置移動。
效能大幅提升,且節點內部的 State 也不會因為位置變更而錯亂!
很多初學者懶得找唯一識別碼,直接寫 items.map((item, index) => <li key={index}>),這會引發嚴重的渲染 Bug:
當列表發生反轉、排序或中間刪除時,項目的 index 就會改變!
0假設刪除了索引為 0 的第一項(原 A,現剩 B)。
對 React 而言,新的第一項(原 B)其 index 依然是 0!
React 誤以為 key="0" 的元素沒被刪除,只是 Props 變了,於是保留了舊 index 0 節點內部的 Uncontrolled State(例如勾選框或 Input 輸入內容)。
結果:畫面上刪除了第一項,但勾選狀態卻留在了新的第一項身上!
永遠使用資料庫 ID 或唯一雜湊:如 item.id 或 crypto.randomUUID()(資料建立時產生)。
絕不能在渲染時動態生成 key:寫成 key={Math.random()} 會導致每次 Re-render 的 key 都不同,React 將強制銷毀並重建所有 DOM 節點,效能直接雪崩。
key 的範圍僅限於同層級兄弟節點 (Siblings):key 不需要全域唯一,只要在同一個父節點的同層級列表中唯一即可。